﻿using System;
using System.Collections.Generic;
using System.Threading;

namespace InfiniTec.Threading
{
    public static class Dispatcher
    {
        private static Thread _DispatcherThread;
        private static readonly List<DispatcherCookie> _PendingCallbacks = new List<DispatcherCookie>();
        private static readonly SpinWaitLock _PendingCallbacksLock = new SpinWaitLock();
        private static readonly AutoResetEvent _DispatcherSignal = new AutoResetEvent(false);

        public static DispatcherCookie RegisterCallback(TimeSpan delay, WaitCallback callback)
        {
            return RegisterCallback(delay, callback, null);
        }

        public static DispatcherCookie RegisterCallback(TimeSpan delay, WaitCallback callback, object state)
        {
            if (delay <= TimeSpan.Zero) throw new ArgumentOutOfRangeException("delay", delay, "The delay must be positive");
            return RegisterCallback(DateTime.Now + delay, callback, state);
        }
        public static DispatcherCookie RegisterCallback(DateTime callbackTime, WaitCallback callback)
        {
            return RegisterCallback(callbackTime, callback, null);
        }

        public static DispatcherCookie RegisterCallback(DateTime callbackTime, WaitCallback callback, object state)
        {
            if (callback == null) throw new ArgumentNullException("callback");
            if (callbackTime < DateTime.Now) throw new ArgumentOutOfRangeException("callbackTime", callbackTime, "The callback time must be in the future.");

            var cookie = new DispatcherCookie(callbackTime, callback, state);

            try
            {
                _PendingCallbacksLock.Enter();
                if (_DispatcherThread == null)
                {
                    InitializeDispatcherThread();
                }

                _PendingCallbacks.Add(cookie);
                _PendingCallbacks.Sort();
            }
            finally
            {
                _PendingCallbacksLock.Exit();
            }
            _DispatcherSignal.Set();

            return cookie;
        }

        public static bool TryCancelCallback(DispatcherCookie cookie)
        {
            if (cookie == null) throw new ArgumentNullException("cookie");

            try
            {
                _PendingCallbacksLock.Enter();
                return _PendingCallbacks.Remove(cookie);

            }
            finally
            {
                _PendingCallbacksLock.Exit();
            }
        }
        public static void CancelCallback(DispatcherCookie cookie)
        {
            if (!TryCancelCallback(cookie))
            {
                throw new ArgumentException("The cookie is not registered on the dispatcher.");
            }
        }

        private static void InitializeDispatcherThread()
        {
            _DispatcherThread = new Thread(DispatcherThread) { Name = "infinitec.threading Dispatcher thread", IsBackground = true };

            _DispatcherThread.Start();
        }

        private static void DispatcherThread()
        {
            while (true)
            {
                DispatcherCookie cookie = null;
                TimeSpan? delay;

                try
                {
                    _PendingCallbacksLock.Enter();
                    DateTime now = DateTime.Now;
                    delay = null;

                    if (_PendingCallbacks.Count > 0)
                    {
                        cookie = _PendingCallbacks[0];
                        if (cookie.CallbackTime <= now)
                        {
                            _PendingCallbacks.RemoveAt(0);
                        }
                        else
                        {
                            delay = cookie.CallbackTime - now;
                            cookie = null;
                        }
                    }
                }
                finally
                {
                    _PendingCallbacksLock.Exit();
                }

                if (delay != null)
                {
                    _DispatcherSignal.WaitOne(delay.Value, true);
                }
                else if (cookie == null)
                {
                    _DispatcherSignal.WaitOne();
                }
                else
                {
                    ThreadPool.QueueUserWorkItem(ExecuteCallback, cookie);
                }
            }
        }

        private static void ExecuteCallback(object state)
        {
            var cookie = (DispatcherCookie)state;

            try
            {
                cookie.Callback(cookie.State);
            }
            catch (Exception ex)
            {
                AsyncOperation.Trace.Error("Failed to execute callback. Error: {0}", ex);
            }
        }
    }
}
